/**
 * 
 */
package jvs.sh;

import info.niwota.vfs.smack.FileSystem;

import java.io.ByteArrayOutputStream;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.zip.Adler32;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;

import jvs.peer.io.ChannelManager;
import jvs.peer.io.FileTransfer.Monitor;
import jvs.peer.sec.AccessControl;
import jvs.peer.sec.Role;
import jvs.peer.sec.User;

/**
 * @author qiangli
 * 
 */
public class Shell implements IShell {
	private class DirectoryFilter implements FileFilter {

		public boolean accept(java.io.File pathname) {
			return pathname.isDirectory();
		}

	}

	public static final String CLASSPATH_SEPARATOR = ";";

	public static DateFormat df = new SimpleDateFormat("yyyy-MM-dd.hh:mm:ss");
	public static FileMan fileman = FileMan.getInstance();

	public static final char[] HEX_TAB = { '0', '1', '2', '3', '4', '5', '6',
			'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

	private static final File ROOTDIR = new File("v:/");

	public static final User ROOTUSER = new User("root", "root");

	public static final String VERBOSE = "verbose";

	private static final File PUBLICDIR = new File("v:/public");

	private static final File GUESTDIR = new File("v:/shared/guest");

	private static char[] byteToHex(byte[] bytes) {
		char[] hex = new char[bytes.length * 2];
		int idx = 0;
		for (byte b : bytes) {
			int v = b & 0xFF;
			hex[idx++] = HEX_TAB[v >>> 4];
			hex[idx++] = HEX_TAB[v & 0xF];
		}
		return hex;
	}
	
	private static void checkConnect(User user, AccessControl acl) {
		//local/console user
		if (acl == null) {
			return;
		}
		
		boolean adminAllowed = OS.isAdminAccessAllowed();
		boolean buddyAllowed = OS.isBuddyAccessAllowed();
		boolean publicAllowed = OS.isPublicAccessAllowed();
		
		//admin user
		String id = user.getId();
		if ((adminAllowed || buddyAllowed || publicAllowed) && acl.checkGroup(id, AccessControl.ADMIN_GROUP)) {
			return;
		}
		//buddy user
		boolean buddy = acl.checkPrincipal(id);
		if ((buddyAllowed || publicAllowed) && buddy) {
			return;
		}
		//public user
		if (publicAllowed) {
			return;
		}
		throw new SecurityException("Connection refused. "+id);
	}

	/**
	 * check auth
	 * @param file
	 */
	private static void checkAuth(File file, User user, AccessControl acl) {
		if (Auth.isAuth(file, user.getId(), acl)) {
			return;
		}
		throw new SecurityException("Not authorized. "+file);
	}
	
	public static String diffstr(String s, String ref) {
		// assume s not null;
		if (ref == null) {
			return s.substring(0, 1);
		}
		int len = Math.min(s.length(), ref.length());
		for (int i = 0; i < len; i++) {
			if (s.charAt(i) != ref.charAt(i)) {
				return s.substring(0, i + 1);
			}
		}
		return s.substring(0, len + 1);
	}
	
	public static char[] digest(File file, String alg) throws IOException,
			NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance(alg);
		byte[] b = new byte[128];
		FileInputStream fis = new FileInputStream(file);
		while (true) {
			int read = fis.read(b);
			if (read == -1) {
				break;
			}
			if (read > 0) {
				md.update(b, 0, read);
			}
		}
		fis.close();
		byte[] h = md.digest();
		return byteToHex(h);
	}

	public static void ensureAdmin(User user, AccessControl acl) {
		if (acl == null) {
			return;
		}
		String id = user.getId();
		if (acl.checkGroup(id, AccessControl.ADMIN_GROUP)) {
			return;
		}
		throw new SecurityException("No privilege");
	}

	private static boolean isRoot(String p) {
		return p.equals("/") || p.equals("v:/");
	}
	
	private static boolean isShared(String p) {
		return p.equals("/shared") || p.equals("v:/shared");
	}
	
	protected AccessControl acl;

	protected boolean connected = false;

	protected File cwd = ROOTDIR;

	protected Map<String, String> env = new TreeMap<String, String>();

	protected Set<Role> roles = new HashSet<Role>();

	protected File root = ROOTDIR;

	private FileMan.Status status = new FileMan.Status();

	protected User user = ROOTUSER;

	protected long pullMax = 5*1024;;

	public Shell() {
	}

	public void abort() throws Exception {
		status.aborted = true;
	}


	public void addNote(File f, String note) throws IOException {
		checkAuth(f);
		NoteHelper.addNote(f.getPath(), user.getName(), user.getId(), note);
	}

	public void ant(File buildfile, String target, boolean verbose,
			Appendable logger) throws Exception {
		ensureAdmin();
		//
	}

	public void attr(File f, StringBuilder sb, String crlf) throws Exception {
		ensureAdmin();
		//
		sb.append("path: " + f.getAbsolutePath());
		sb.append(crlf);
		sb.append("size: " + f.length());
		sb.append(crlf);
		sb.append("modified: " + df.format(new Date(f.lastModified())));
		sb.append(crlf);
		sb.append("uri: " + f.toRealURI().toString());
		sb.append(crlf);
		sb.append("read: " + f.canRead());
		sb.append(crlf);
		sb.append("write: " + f.canWrite());
		sb.append(crlf);
		sb.append("mount point: "
				+ FileSystem.getFileSystem().isMountPoint(f.getPath()));
		sb.append(crlf);
	}


	public void bsh(File f, ByteArrayOutputStream bos) throws Exception {
//		ensureAdmin();
//		//
//		final PrintStream out = new PrintStream(bos);
//		final StringReader in = new StringReader("");
//		bsh.Interpreter interp = new bsh.Interpreter(in, out, out, false, null);
//		interp.setClassLoader(this.getClass().getClassLoader());
//		interp.source(f.getPath());
	}

	public void bsh(String data, ByteArrayOutputStream bos) throws Exception {
//		ensureAdmin();
//		//
//		final PrintStream out = new PrintStream(bos);
//		final StringReader in = new StringReader("");
//		bsh.Interpreter interp = new bsh.Interpreter(in, out, out, false, null);
//		interp.setClassLoader(this.getClass().getClassLoader());
//		interp.eval(data);
	}


	public void buzz(String param) {
		//TODO
	}

	public String cat(File f) throws IOException {
		checkAuth(f);
		
		String s = fileman.readlines(f, Integer.MAX_VALUE);
		return s;
	}

	public void chdir() throws IOException {
		chdir(getHome());
	}
	
	public void chdir(File f) throws IOException {
		checkAuth(f);
		cwd = new File(f.getCanonicalPath());
	}

	public void checkAuth(File f) {
		checkAuth(f, user, acl);
	}

	private long checksum(File file, Checksum checksum) throws IOException {
		CheckedInputStream cis = new CheckedInputStream(new FileInputStream(
				file), checksum);
		byte[] b = new byte[128];
		while (cis.read(b) >= 0) {
		}
		long cs = cis.getChecksum().getValue();
		cis.close();
		return cs;
	}

	public String checksum(File file, String arg) throws IOException,
			NoSuchAlgorithmException {
		checkAuth(file);
		
		Checksum cs = new CRC32();
		if (arg == null || arg.equalsIgnoreCase("crc32")) {
			return Long.toBinaryString(checksum(file, cs));
		} else if (arg.equalsIgnoreCase("adler32")) {
			cs = new Adler32();
			return Long.toBinaryString(checksum(file, cs));
		}
		return new String(digest(file, arg));
	}

	public File[] classpath() {
		ensureAdmin();
		//
		String cpe = env.get("classpath");
		String[] cps = null;
		if (cpe == null || (cps = cpe.split(CLASSPATH_SEPARATOR)) == null) {
			return new File[0];
		}
		ArrayList<File> files = new ArrayList<File>(cps.length);
		for (String cp : cps) {
			try {
				files.add(resolveFile(cp));
			} catch (Exception e) {
			}
		}
		return files.toArray(new File[0]);
	}

	public void connect(User user) {
		checkConnect(user, acl);
		//
		this.connected = true;
		this.user = user;
		this.user.setConnectionTime(System.currentTimeMillis());
	}

	public void copy(File from, File to) throws IOException {
		checkAuth(from);
		checkAuth(to);
		
		fileman.copy(from, to);
	}


	public String date(String fmt) {
		DateFormat df = new SimpleDateFormat(fmt);
		return df.format(new Date());
	}

	public String decode(String s) throws UnsupportedEncodingException {
		return URLDecoder.decode(s, "UTF-8");
	}

	public boolean delete(File f) {
		checkAuth(f);
		
		return f.delete();
	}

	public void dex(String param) throws IOException {
		ensureAdmin();
		//TODO
	}

	public void df(StringBuilder sb, String crlf) {
		ensureAdmin();
		
		Map<String, Entry> map = FileSystem.getFileSystem().getMountpoints();
		for (Entry e : map.values()) {
			sb.append(e.toString());
			sb.append(crlf);
		}
	}

	public int dir(File f, StringBuilder sb, String crlf) {
		checkAuth(f);
		
		java.io.File[] files = f.listFiles(new DirectoryFilter());
		for (java.io.File i : files) {
			sb.append(i.getName());
			sb.append(crlf);
		}
		return files.length;
	}

//	public long du(File f, Collector<File> trail) {
//		checkAuth(f);
//		
//		status.aborted = false;
//		long t = fileman.du(f, trail, status);
//		return t;
//	}

	public void edit(String txt, File f) throws IOException {
		checkAuth(f);
		fileman.saveas(txt, f);
	}

	public String encode(String s) throws UnsupportedEncodingException {
		return URLEncoder.encode(s, "UTF-8");
	}

	public void ensureAdmin() {
		ensureAdmin(user, acl);
	}
	
	/**
	 * @return true if current user is an accepted buddy
	 */
	public void ensureUser() {
		if (acl.checkPrincipal(user.getId())) {
			return;
		}
		throw new RuntimeException("Not authorized");
	}

	public Map<String, String> env() {
		return env;
	}

	public void exit() {
		try {
			this.connected = false;
			this.acl = null;
			this.roles.clear();
			this.roles = null;
			this.env.clear();
			this.env = null;
			this.root = null;
			this.cwd = null;
			this.user = null;
			//
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

//	public void find(File dir, String pattern, Collector<File> out,
//			Collector<File> trail) {
//		checkAuth(dir);
//		
//		status.aborted = false;
//		fileman.find(dir, pattern, out, trail, status);
//	}

	public String format(Date date) {
		return df.format(date);
	}

	public File getCwd() {
		return cwd;
	}

	public String getEnv(String n) {
		return env.get(n);
	}

	public String getName() {
		return "";
	}

	public Set<Role> getRoles() {
		return roles;
	}

	public File getRoot() {
		return root;
	}

	public User getUser() {
		return user;
	}

	public Role grant(Role.Type type, String group) {
		ensureAdmin();
		
		Role g = new Role(type, group);
		roles.add(g);
		return g;
	}

//	public void grep(File f, String pattern, Collector<String> out)
//			throws IOException {
//		checkAuth(f);
//		
//		fileman.grep(f, pattern, out);
//	}

	public String head(File f, int max) throws IOException {
		checkAuth(f);
		
		return fileman.readlines(f, max);
	}

	public String help() {
		return Man.help(null);
	}

	public String help(String cmd) {
		return Man.help(cmd);
	}

	public void icon(File f, OutputStream data) throws Exception {
		throw new IOException("Not supported");
	}
	
	public String id() {
		return user.getId();
	}

	public void info(File f, StringBuilder sb, String crlf) {
		if (isAuth(f)) {
			File.format(f, sb);
			sb.append(crlf);
			return;
		}
		//allow users to see root and shared
		String p = f.getPath();
		if (isRoot(p)) {
			File.format(f, sb);
			sb.append(crlf);
			return;
		}
		//
		if (isShared(p)) {
			File.format(f, sb);
			sb.append(crlf);
			return;
		}
		//no access
		throw new SecurityException("Not authorized. "+f);
	}

	public void init() {
		env.put(VERBOSE, "true");
	}

	public boolean isConnected() {
		return connected;
	}
	
	protected boolean isAuth(File f) {
		return Auth.isAuth(f, user.getId(), acl);
	}

	public boolean isVerbose() {
		String s = env.get(VERBOSE);
		return (s != null && Boolean.parseBoolean(s));
	}

	public void java(String[] args) throws Exception {
		ensureAdmin();
		
		URL[] urls = toUrls(classpath());
		ClassLoader cl = new ShellClassLoader(urls, this.getClass()
				.getClassLoader());
		Class<?> c = Class.forName(args[0], true, cl);
		Method m = c.getDeclaredMethod("main", new Class[] { String[].class });
		m.setAccessible(true);
		String[] mainargs = new String[args.length - 1];
		if (mainargs.length > 0) {
			System.arraycopy(args, 1, mainargs, 0, mainargs.length);
		}
		m.invoke(null, new Object[] { mainargs });
	}

	public void javac(File src, File outdir, Appendable logger)
			throws Exception {
		ensureAdmin();
		//TODO
	}

	public void link(File f, String u, String opt) throws URISyntaxException,
			IOException {
		ensureAdmin();
		//
		URI uri = new URI(u);
		f.link(uri, opt);
	}

	public int list(File file, int off, int max, StringBuilder out) {
		if (isAuth(file)) {
			java.io.File[] files = file.listFiles();
			for (int i = off; i < files.length && i < off + max; i++) {
				File fi = (File) files[i];
				formatln(out, fi);
			}
			return files.length;
		}
		//we handle v:/ and /shared a little differently
		//since users always know they exist 
		String p = file.getPath();
		if (isRoot(p)) {
			int cnt = 0;
			File pub = new File("/public");
			if (pub.exists()) {
				formatln(out, pub);
				cnt++;
			}
			File shared = new File("/shared");
			if (shared.exists()) {
				formatln(out, shared);
				cnt++;
			}
			return cnt;
		}
		if (isShared(p)) {
			java.io.File[] files = file.listFiles();
			int cnt = 0;
			for (int i = off; i < files.length && i < off + max; i++) {
				if (isAuth((File)files[i])) {
					File fi = (File) files[i];
					formatln(out, fi);
					cnt++;
				}
			}
			return cnt;
		}
		//no access
		throw new SecurityException("Not authorized. "+file);
	}

//	public int listi(File file, int pagesize, Collector<Index> indice) {
//		checkAuth(file);
//		
//		return file.listi(pagesize, indice);
//	}
	
//	public int listp(File file, Pattern pattern, Collector<File> out) {
//		checkAuth(file);
//		
//		File[] files = file.listFiles();
//		for (int i = 0; i < files.length; i++) {
//			File f = files[i];
//			String n = f.getName();
//			if (pattern == null || pattern.matcher(n).matches()) {
//				out.add(f);
//			}
//		}
//		return files.length;
//	}

//	public int listZip(File file, int off, int max, Collector<ZipEntry> out)
//			throws IOException {
//		checkAuth(file);
//		
//		ZipFile zf = new ZipFile(file);
//		int total = 0;
//		int cnt = 0;
//		for (Enumeration<?> entries = zf.entries(); entries.hasMoreElements();) {
//			ZipEntry ze = (ZipEntry) entries.nextElement();
//			total++;
//			if (total > off && cnt < max) {
//				out.add(ze);
//				cnt++;
//			}
//		}
//		return (total);
//	}

//	public int listZip(File file, Pattern pattern, Collector<ZipEntry> out)
//			throws IOException {
//		checkAuth(file);
//		
//		ZipFile zf = new ZipFile(file);
//		int total = 0;
//		for (Enumeration<?> entries = zf.entries(); entries.hasMoreElements();) {
//			ZipEntry ze = (ZipEntry) entries.nextElement();
//			total++;
//			String n = ze.getName();
//			if (pattern == null || pattern.matcher(n).matches()) {
//				out.add(ze);
//			}
//		}
//		return (total);
//	}

	private void formatln(StringBuilder out, File f) {
		File.format(f, out);
		out.append("\r\n");
	}

	public void mail(String to, File f) throws IOException {
		checkAuth(f);
		//TODO
	}

	public boolean mkdirs(File f) {
		checkAuth(f);
		
		return f.mkdirs();
	}

	public String motd() throws IOException {
		File f = new File("v:/my/motd.txt");
		if (f.exists()) {
			return fileman.readlines(f, Integer.MAX_VALUE);
		}
		return null;
	}

	public void mount(File mp, String uri, String opt)
			throws URISyntaxException {
		ensureAdmin();
		
		URI u = new URI(uri);
		FileSystem.getFileSystem().mount(mp.getPath(), u, opt);
	}

	public boolean move(File from, File to) {
		checkAuth(from);
		checkAuth(to);
		
		return from.renameTo(to);
	}

	public NoteIterator notes(File f, String crlf) throws IOException {
		checkAuth(f);
		
		return NoteHelper.notes(f.getPath(), crlf);
	}

	public boolean ping(String peer) throws Exception {
		return ChannelManager.getInstance().ping(peer);
	}

	public String plan() throws IOException {
		File f = new File("v:/my/plan.txt");
		String s = null;
		if (f.exists()) {
			s = fileman.readlines(f, Integer.MAX_VALUE);
		}
		return s;
	}

	public void pull(File f, OutputStream out) throws IOException {
		checkAuth(f);
		
		long len = f.length();
		if (len > pullMax ) {
			throw new IOException("File size limit exceeded. " + pullMax);
		}
		InputStream in = new FileInputStream(f);
		byte[] buf = new byte[512];
		while (true) {
			int read = in.read(buf);
			if (read == -1) {
				break;
			}
			if (read > 0) {
				out.write(buf, 0, read);
				out.flush();
			}
		}
		out.flush();
	}

	public Monitor receive(File f) throws Exception {
		throw new RuntimeException("Not supported");
	}

	public Monitor receive(String from, File file) throws Exception {
		throw new RuntimeException("Not supported");
	}

	public File resolveFile(String path) {
		// TODO
		if (path == null) {
			return cwd;
		}
		File f = null;
		try {
			path = URLDecoder.decode(path, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			//
		}
		if (path.startsWith("/")) {
			f = new File(root, path);
		} else {
			f = new File(cwd, path);
		}

		if (f.getPath().startsWith(root.getPath())) {
			return f;
		}
		return cwd;
	}

	public void revoke(Role.Type type, String group) {
		ensureAdmin();
		
		Role g = new Role(type, group);
		roles.remove(g);
	}

	public boolean rmdir(File f) {
		checkAuth(f);
		
		return f.delete();
	}

	public Collection<Role> roles() {
		return roles;
	}

	public Monitor send(String to, File file) throws Exception {
		throw new RuntimeException("Not supported");
	}

	public void setAccessControl(AccessControl acl) {
		this.acl = acl;
	}

	public void setCwd(File cwd) {
		this.cwd = cwd;
	}

	public void setEnv(String n, String v) {
		env.put(n, v);
	}

	public void setLastAccess(long lastAccess) {
		if (user != null) {
			user.setLastAccess(lastAccess);
		}
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}

	public void setRoot(File root) {
		this.root = root;
	}

	public void shell(String param) {
		throw new RuntimeException("Not supported");
	}

	public void terminate(User user) throws Exception {
	}

	public boolean touch(File f) {
		checkAuth(f);
		
		return f.setLastModified(System.currentTimeMillis());
	}

	private URL[] toUrls(File[] files) throws MalformedURLException {
		URL[] urls = new URL[files.length];
		for (int i = 0; i < files.length; i++) {
			urls[i] = files[i].toURL();
		}
		return urls;
	}

	public void unlink(File f) {
		ensureAdmin();
		
		FileSystem.getFileSystem().unmount(f.getPath());
	}

	public void unmount(String mp) {
		ensureAdmin();
		
		FileSystem.getFileSystem().unmount(mp);
	}

	public String unset(String n) {
		return env.remove(n);
	}
	
	public List<User> who() throws Exception {
		//TODO
		return null;
	}
	
	public void write(String to, String msg) throws Exception {
		throw new RuntimeException("Not supported");
	}

	/**
	 */
	public File getHome() {
		if (acl == null) {
			return ROOTDIR;
		}
		String id = user.getId();
		if (acl.checkGroup(id, AccessControl.ADMIN_GROUP) && OS.isAdminAccessAllowed()) {
			return ROOTDIR;
		}
		//
		if (acl.checkPrincipal(id)) {
			File home = new File("v:/shared/~"+id);
			if (home.exists()) {
				return home;
			} else {
				return GUESTDIR;
			}
		}
		//
		return PUBLICDIR;
	}

	/**
	 * @return list of all groups for admin user or for regular users, list of groups the current user belongs in
	 */
	public List<Group> groups() {
		ensureService();
		
		String id = user.getId();
		if (acl.checkGroup(id, AccessControl.ADMIN_GROUP)) {
			return acl.groups();
		}
		return acl.getGroups(id);
	}

	private void ensureService() {
		if (acl == null) {
			throw new RuntimeException("Service not available");
		}
	}

	public List<Group> groups(String uid) {
		ensureService();
		
		String id = user.getId();
		if (uid == null || uid.equalsIgnoreCase(id)) {
			return acl.getGroups(id);
		}
		
		ensureAdmin();

		return acl.getGroups(uid);
	}

	public void addGroup(String group) {
		ensureAdmin();
		ensureService();

		acl.addGroup(group);
	}

	public void removeGroup(String group) {
		ensureAdmin();
		ensureService();
		
		acl.removeGroup(group);
	}

	public void updateGroup(String oldid, String newid) {
		ensureAdmin();
		ensureService();

		acl.updateGroup(oldid, newid);
	}

	public List<User> users() {
		ensureAdmin();
		ensureService();
		
		return acl.users();
	}

	public List<User> users(String group) {
		ensureAdmin();
		ensureService();

		return acl.getPrincipals(group);
	}

	public void addUser(String uid, String name, String gid) {
		ensureAdmin();
		ensureService();
		
		acl.addPrincipal(uid, name, gid);
	}

	public void removeUser(String uid, String gid) {
		ensureAdmin();
		ensureService();

		acl.removePrincipal(uid, gid);
	}

	/**
	 * update nickname
	 */
	public void updateUser(String uid, String name) {
		ensureAdmin();
		ensureService();

		acl.updateUser(uid, name);
	}

	public void thumbnail(File f, OutputStream data) throws Exception {
		throw new IOException("Not supported");
	}
}
